package sys.knot.configuration;

import com.esotericsoftware.yamlbeans.YamlReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sys.knot.interpreter.ResultSetInterpreter;
import sys.knot.model.ParameterType;
import sys.knot.model.QueryParameter;
import sys.knot.model.QuerySpecification;
import sys.knot.util.DataSourceHelper;
import sys.knot.util.KnotPaths;
import sys.knot.validator.Validator;

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ServiceConfiguration {
    private String serviceEndPoint;
    private String serviceMethod;
    private int limitResultRows;
    private List<QuerySpecification> querySpecifications = new ArrayList<>();
    private Path configFile;

    private Logger logger = LoggerFactory.getLogger(ServiceConfiguration.class);

    private KnotPaths knotPaths;

    private ClassLoader loaderWithPlugins;
    private final String JAR_PATTERN = "*.jar";

    private final Pattern SQL_PARAMETER_PATTERN = Pattern.compile("\\$\\{(.+?)\\:(.+?)\\}");
    private final Pattern VALIDATOR_PARAMETER_PATTERN = Pattern.compile("(.+?)\\((.+?)\\)");


    public ServiceConfiguration(Path configFile, KnotPaths knotPaths) throws ConfigurationException {
        this.configFile = configFile;
        this.knotPaths = knotPaths;

        RawConfig rawConfig = readFile(configFile);
        rawConfig.validate();

        DataSourceHelper dataSourceHelper = getDataSourceHelper();

        this.serviceMethod   = rawConfig.method;
        this.serviceEndPoint = rawConfig.url;
        this.limitResultRows = rawConfig.limitRows;

        Map<String, List<Validator>> validators  = loadValidators(rawConfig.indataValidators);

        for(RawConfig.Query rawQuery : rawConfig.queries) {
            QuerySpecification querySpecification = new QuerySpecification();
            querySpecification.setName(rawQuery.queryName);

            querySpecification.setDataSources(dataSourceHelper.getMatchingDataSourcees(rawQuery.datasources));

            querySpecification.setResultSetInterpreter(loadInterpreter(rawQuery.resultInterpreter));

            //sql and parameters, with validators
            List<QueryParameter> orderedQueryParameters = parseQueryParameters(rawQuery.sql);
            addValidatorsToQueryParameters(orderedQueryParameters, validators);
            querySpecification.setSql(removeParameterNamesFromSql(rawQuery.sql, orderedQueryParameters));
            querySpecification.setOrderedQueryParameters(orderedQueryParameters);

            this.querySpecifications.add(querySpecification);
        }

    }

    private void addValidatorsToQueryParameters(List<QueryParameter> orderedQueryParameters, Map<String, List<Validator>> validators) throws ConfigurationException {
        if (orderedQueryParameters != null && validators != null) {
            for (QueryParameter p : orderedQueryParameters) {
                List<Validator> parameterValidators = validators.get(p.getName());
                if (parameterValidators != null) {
                    for (Validator v : parameterValidators) {
                        List<ParameterType> types = v.getSupportedTypes();
                        if (!types.contains(p.getType())) {
                            throw new ConfigurationException("Validator " + v + " does not support the type " +
                                    p.getType() + " used by parameter " + p.getName());
                        }
                    }
                    p.setValidators(parameterValidators);
                }
            }
        }
    }

    private Map<String, List<Validator>> loadValidators(List<RawConfig.InputValidator> rawValidators) throws ConfigurationException {
        Map<String, List<Validator>> result = new HashMap<>();

        if (rawValidators != null) {
            for (RawConfig.InputValidator raw : rawValidators) {

                List<Validator> list = new ArrayList<>();
                for (String indataValidator : raw.validators) {
                    Matcher matcher = VALIDATOR_PARAMETER_PATTERN.matcher(indataValidator);
                    matcher.find();
                    String className = matcher.group(1);
                    String[] params = matcher.group(2).split("\\s*,\\s*");

                    Validator v = (Validator) loadClass(className);
                    v.addValidatorParameters(params);

                    list.add(v);
                }
                result.put(raw.parameter, list);
            }
        }
        return result;
    }

    private ResultSetInterpreter loadInterpreter(String resultInterpreter) throws ConfigurationException {
        return (ResultSetInterpreter) loadClass(resultInterpreter);
    }

    private Object loadClass(String fullName) throws ConfigurationException {
        try {
            if (loaderWithPlugins == null) {
                List<URL> urlList = new ArrayList<>();
                DirectoryStream<Path> ds = Files.newDirectoryStream(knotPaths.getPluginDir(), JAR_PATTERN);
                for (Path p : ds) {
                    logger.info("Loading plugin jar: " + p);
                    urlList.add(p.toUri().toURL());
                }

                loaderWithPlugins = URLClassLoader.newInstance(
                        urlList.toArray(new URL[urlList.size()]),
                        getClass().getClassLoader()
                );
            }

            return Class.forName(fullName, true, loaderWithPlugins).newInstance();
        } catch (NullPointerException | IOException | InstantiationException | IllegalAccessException | ClassNotFoundException e) {
            throw new ConfigurationException("Error loading class: " + fullName, e);
        }
    }


    private RawConfig readFile(Path configFile) throws ConfigurationException {
        try {
            YamlReader reader = new YamlReader(Files.newBufferedReader(configFile));
            reader.getConfig().setPropertyElementType(RawConfig.class, "indataValidators", RawConfig.InputValidator.class);
            reader.getConfig().setPropertyElementType(RawConfig.class, "queries", RawConfig.Query.class);
            return reader.read(RawConfig.class);
        } catch (IOException e) {
            throw new ConfigurationException("Could not read config file: " + configFile, e);
        }
    }

    private String removeParameterNamesFromSql(String sql, List<QueryParameter> queryParameters) {
        for (QueryParameter queryParameter : queryParameters) {
            sql = sql.replace("${"+queryParameter.getType() + ":" +queryParameter.getName()+"}", "?");
        }
        return sql;
    }

    private List<QueryParameter> parseQueryParameters(String sql) {
        final List<QueryParameter> result = new ArrayList<>();
        final Matcher matcher = SQL_PARAMETER_PATTERN.matcher(sql);
        while (matcher.find()) {

            result.add(new QueryParameter(matcher.group(2), matcher.group(1)));
        }
        return result;
    }

    public String getServiceEndPoint() {
        return serviceEndPoint;
    }

    public String getServiceMethod() {
        return serviceMethod;
    }

    public int getLimitResultRows() {
        return limitResultRows;
    }

    public List<QuerySpecification> getQuerySpecifications() {
        return querySpecifications;
    }

    @Override
    public String toString() {
        return serviceMethod + ":" + serviceEndPoint;
    }

    public void echoDetails() {
        logger.info("Loaded config: " + configFile);
        logger.info(" - serviceMethod: " + serviceMethod);
        logger.info(" - serviceEndPoint: " + serviceEndPoint);
        logger.info(" - limitResultRows: " + limitResultRows);
        for (QuerySpecification s : querySpecifications) {
            logger.info(" - query: ");
            logger.info(" - - name: " + s.getName());
            logger.info(" - - sql: " + s.getSql());
            logger.info(" - - orderedQueryParameters: " + s.getOrderedQueryParameters());
            logger.info(" - - resultInterpreter: " + s.getResultSetInterpreter());
            logger.info(" - - dataSources: " + s.getDataSources());
        }
    }

    protected DataSourceHelper getDataSourceHelper() {
        return new DataSourceHelper();
    }
}
